#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <iostream>
#include <fstream>
#include <sstream>

#define ASSERT(x) \
    if (!(x)) __debugbreak();

#define GLCall(x)   \
    GLClearError(); \
    x;              \
    ASSERT(GLLogCall(#x, __FILE__, __LINE__))

static auto GLClearError() -> void {
    while (glGetError() != GL_NO_ERROR)
        ;
}

static auto GLLogCall(const char *function, const char *file, int line)
    -> bool {
    while (GLenum error = glGetError()) {
        std::cout << "[OpenGL Error] (" << error << "): " << function << " "
                  << file << ": " << line << std::endl;
        return false;
    }
    return true;
}

struct shaderProgramSrc {
    std::string vertexSrc;
    std::string fragmentSrc;
};

static auto parseShader(const std::string &filepath) -> shaderProgramSrc {
    std::ifstream stream(filepath);
    if (!stream.is_open()) {
        std::cerr << "Failed to open shader file: " << filepath << std::endl;
        return {};
    }

    enum class shaderType { NONE = -1, VERTEX = 0, FRAGMENT = 1 };
    std::string line;
    std::stringstream ss[2];
    shaderType type = shaderType::NONE;
    while (getline(stream, line)) {
        if (line.find("#shader") != std::string::npos) {
            if (line.find("vertex") != std::string::npos) {
                // set to vertex shader
                type = shaderType::VERTEX;
            } else if (line.find("fragment") != std::string::npos) {
                // set to fragment shader
                type = shaderType::FRAGMENT;
            }
        } else {
            ss[static_cast<int>(type)] << line << '\n';
        }
    }

    return {ss[0].str(), ss[1].str()};
}

static auto compileShader(unsigned int type, const std::string &source)
    -> unsigned int {
    auto id = glCreateShader(type);
    auto src = source.c_str();
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    // some error handing:
    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result);
    if (result == GL_FALSE) {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
        auto message = static_cast<char *>(alloca(length * sizeof(char)));
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Failed to compile "
                  << (type == GL_VERTEX_SHADER ? "vertex" : "fragment")
                  << " shader" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);

        return 0;
    }

    return id;
}

static auto createShader(const std::string &vertexShader,
                         const std::string &fragmentShader) -> unsigned int {
    auto program = glCreateProgram();
    auto vs = compileShader(GL_VERTEX_SHADER, vertexShader);
    auto fs = compileShader(GL_FRAGMENT_SHADER, fragmentShader);

    // attach shader to program
    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    glValidateProgram(program);

    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}
// NOLINTBEGIN
int main(void) {
    // NOLINTEND
    GLFWwindow *window;

    /* Initialize the library */
    if (glfwInit() == GLFW_FALSE) {
        return -1;
    }

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(800, 600, "Hello World", nullptr, nullptr);
    if (window == nullptr) {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);  // create openGL context

    glfwSwapInterval(1);

    if (glewInit() != GLEW_OK) {
        std::cout << "glew Init error\n";
    }
    std::cout << glGetString(GL_VERSION) << std::endl;

    float positions[] = {
        -0.5f, -0.5f,  // 0
        0.5f,  -0.5f,  // 1
        0.5f,  0.5f,   // 2
        -0.5f, 0.5f    // 3
    };

    unsigned int indices[] = {
        0, 1, 2,  //
        2, 3, 0   //
    };

    unsigned int vao;
    GLCall(glGenVertexArrays(1, &vao));
    GLCall(glBindVertexArray(vao));

    unsigned int buffer;
    GLCall(glGenBuffers(1, &buffer));
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(float), positions,
                 GL_STATIC_DRAW);

    GLCall(glEnableVertexAttribArray(0));  // after binding
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, nullptr);

    unsigned int ibo;
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices,
                 GL_STATIC_DRAW);

    auto src = parseShader("shaders/basic.shader");
    auto shader = createShader(src.vertexSrc, src.fragmentSrc);
    glUseProgram(shader);

    std::cout << "VERTEX" << std::endl;
    std::cout << src.fragmentSrc << std::endl;
    std::cout << "FRAGMENT" << std::endl;
    std::cout << src.vertexSrc << std::endl;

    auto location = glGetUniformLocation(shader, "u_color");
    ASSERT(location != -1);

    GLCall(glUniform4f(location, 0.8f, 0.3f, 0.8f, 1.0f));

    GLCall(glBindVertexArray(0));
    GLCall(glUseProgram(0));
    GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
    GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

    auto r = 0.0f;
    auto increment = 0.05f;

    /* Loop until the user closes the window */
    // NOLINTBEGIN
    while (!glfwWindowShouldClose(window)) {
        // NOLINTEND
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);
        GLCall(glUseProgram(shader));

        glUniform4f(location, r, 0.3f, 0.8f, 1.0f);

        GLCall(glBindVertexArray(vao));
        GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));

        GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));

        if (r > 1.0f) {
            increment = -0.05f;
        } else if (r < 0) {
            increment = 0.05f;
        }
        r += increment;

        /* Swap front and back buffers */
        glfwSwapBuffers(window);

        /* Poll for and process events */
        glfwPollEvents();
    }

    glDeleteProgram(shader);

    glfwTerminate();
    return 0;
}